class Compile {
  /**
   * 构造函数
   * @param {* 要解析的元素} el 
   * @param {* 当前vue的实例} vm 
   */
  constructor(el, vm) {
    this.$el = document.querySelector(el) // 选择元素
    this.$vm = vm // 保存当前的vue实例

    // 编译
    if (this.$el) { // 判断当前要解析的元素是否存在，存在则进行编译
      // 将当前元素的内容转化为片段fragment
      this.$fragment = this.node2Fragment(this.$el)

      // 执行编译
      this.compile(this.$fragment)

      // 将编译完的内容追加值$el
      this.$el.appendChild(this.$fragment)
    }
  }
  /**
   * 将要解析的dom元素转化为片段
   * @param {* 要解析的dom元素} el 
   */
  node2Fragment(el) {
    // 创建一个文档碎片，用于存放el中所有的族元素
    const fragment = document.createDocumentFragment();

    let child = null

    while (child = el.firstChild) {
      // 将要解析的元素片段全部搬家至创建的文档碎片中
      fragment.appendChild(child)
    }
    return fragment
  }
  /**
   * 对虚拟dom进行编译
   * @param {* 片段化的虚拟dom} fragment 
   */
  compile(fragment) {
    const childNodes = fragment.childNodes
    Array.from(childNodes).forEach(item => {
      if (this.isElement(item)) {
        // 元素
        // 如果当前节点是一个元素，则将该元素的所有属性取出
        const nodeAttrs = item.attributes;
        Array.from(nodeAttrs).forEach(attr => {
          const attrName = attr.name // 保存当前元素的属性名
          const exp = attr.value // 保存当前元素的属性值

          if (this.isDirective(attrName)) {
            // l-text l-html...
            const dir = attrName.substring(2) // 如果是指令，取出l-之后的部分

            // 执行指令
            this[dir] && this[dir](item, this.$vm, exp)
          }
          if (this.isEvent(attrName)) {
            // @click...
            const dir = attrName.substring(1)
            this.handlerEvent(item, this.$vm, exp, dir)
          }
        });
      } else if (this.isInterPolation(item)) {
        // 插值文本
        // console.log(`编译文本${item.textContent}`)
        // 对插值文本内容进行编译
        this.compileText(item)
      }

      // 对子节点进行递归
      if (item.childNodes) {
        // 如果当前节点存在子节点，对其进行递归
        this.compile(item)
      }
    })
  }

  /**
   * 文本指令
   * @param {* 使用该指令的节点} node 
   * @param {*} vm 
   * @param {* 该节点的属性值表达式} exp 
   */
  text(node, vm, exp) {
    this.update(node, vm, exp, 'text')
  }

  /**
   * 事件指令
   * @param {* 使用该指令的节点} node 
   * @param {*} vm 
   * @param {* 该节点属性值的表达式} exp 
   * @param {* 使用了哪种事件} dir 
   */
  handlerEvent(node, vm, exp, dir) {
    // 如果当前的vue实例中有methods，将其中被触发的方法的方法名取出
    let fn = vm.$options.methods && vm.$options.methods[exp]

    if (dir && fn) {
      node.addEventListener(dir, fn.bind(vm))
    }

  }

  /**
   * 双向绑定
   * @param {* 绑定给该指令的节点} node 
   * @param {*} vm 
   * @param {* 该节点属性值的表达式} exp 
   */
  model(node, vm, exp) {
    this.update(node, vm, exp, 'model')

    node.addEventListener('input', e => {
      vm[exp] = e.target.value
    })
  }

  /**
   * 双向绑定的数据更新
   * @param {*绑定该指令的节点} node 
   * @param {* 更新后的值} value 
   */
  modelUpdate(node, value) {
    node.value = value
  }

  /**
   * 
   * @param {* 绑定html指令的节点} node 
   * @param {*} vm 
   * @param {* html属性值的表达式} exp 
   */
  html(node, vm, exp) {
    this.update(node, vm, exp, 'html')
  }

  /**
   * 
   * @param {* 绑定html指令的节点} node 
   * @param {* 更新后的值} value 
   */
  htmlUpdate(node, value) {
    node.innerHTML = value
  }

  /**
   * 判断当前节点是否是元素
   * @param {* 要判断类型的节点} node 
   */
  isElement(node) {
    return node.nodeType === 1
  }

  /**
   * 判断当前节点是否是插值文本
   * @param {* 要判断类型的节点} node 
   */
  isInterPolation(node) {
    // 如果当前节点是文本，那么判断其是否是插值文本，过滤掉静态文本
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }

  /**
   * 对插值问本的内容进行编译
   * @param {* 插值文本的内容} text 
   */
  compileText(node) {
    this.update(node, this.$vm, RegExp.$1, 'text')
  }

  /**
   * 更新函数
   * @param {* 更新的节点} node 
   * @param {* vue的实例} vm 
   * @param {* 表达式} exp 
   * @param {* 指令} dir 判断是文本，事件，还是其他的*** 
   */
  update(node, vm, exp, dir) {
    // 具体的更新函数 textupdate htmlupdate ...
    const updateFn = this[dir + 'Update']

    // 初始化
    updateFn && updateFn(node, vm.$data[exp])

    // 依赖收集
    new Watcher(vm, exp, function (value) {
      updateFn && updateFn(node, value)
    })
  }

  /**
   * 更新文本节点
   * @param {* 要更新的节点} node 
   * @param {* 新的节点值} value 
   */
  textUpdate(node, value) {
    return node.textContent = value
  }

  /**
   * 判断熟悉名是否是一个指令 l-text l-html...
   * @param {* 元素的属性名} attrName 
   */
  isDirective(attrName) {
    // 如果以l-开头，则为指令
    return attrName.indexOf('l-') === 0
  }

  isEvent(attrName) {
    // 如果以@开头，则为事件
    return attrName.indexOf('@') === 0
  }
}